package com.gao.d_selector.f;


import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @author gao
 * @time 2020/08/13 20:40:15
 */
public class Server {
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;

    public Server(int port) throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(port));
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("The server is started...");
    }

    public void start() {
        try {
            while (true) {
                int n = selector.select();
                if (n > 0) {
                    Set<SelectionKey> selectedKeys = selector.selectedKeys();
                    Iterator<SelectionKey> it = selectedKeys.iterator();
                    while (it.hasNext()) {
                        SelectionKey key = it.next();
                        it.remove();
                        if (key.isValid() && key.isAcceptable()) {
                            accept(key);
                        }
                        // 在if中，调用key.isValid()的价值在于，有可能key在上一个处理中（就是上面的if中的处理），
                        // 被cancel了，所以这里一定要判断一个key是否还是有效的。
                        if (key.isValid() && key.isReadable()) {
                            read(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // ===============================read测试范围开始================================================
    // bodyLength如果取值为-1，表示则当前读是一个全新的读的开始。如果不是-1，则表示本次读的上一次读发生了断包
    private int bodyLength = -1;
    // 在断包发生的情况下，bodyLength2将会是bodyLength的备份
    private int bodyLength2 = 0;
    private byte[] headBytes = new byte[4];
    private byte[] bodyBytes = null;
    private ByteBuffer cacheBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer buffer = ByteBuffer.allocate(100);

    private void read(SelectionKey key) {
        try {
            SocketChannel channel = (SocketChannel) key.channel();

            if (bodyLength == -1) {
                int n = channel.read(buffer);
                buffer.flip();
                buffer.get(headBytes);
                bodyLength = Utils.byteToInt(headBytes);

                if (bodyLength <= buffer.remaining()) {
                    // bodyLength <= buffer.remaining()，说明buffer中剩余的内容，囊括了一条完整的消息，可以直接读取出这个完整的消息
                    bodyBytes = new byte[bodyLength];
                    buffer.get(bodyBytes);
                    String msg = new String(bodyBytes);
                    System.out.println(msg + " 粘包处理");

                    if (buffer.hasRemaining()) {
                        System.out.println("发生粘包");
                        // buffer.hasRemaining()返回true，说明此处发生了“粘包”
                        // 也就是说，buffer剩余的内容，是下一个消息的开头的部分数据，
                        // 这里把剩余的数据，移动到buffer的开头
                        buffer.compact();
                    } else {
                        // buffer.hasRemaining()返回false，说明读取数据完毕。
                        // 此处应该清空buffer
                        buffer.clear();
                    }
                    bodyLength = -1;
                } else {
                    System.out.println("发生断包");
                    bodyLength2 = bodyLength;
                    // bodyLength > buffer.remaining()，说明buffer中剩余的内容，无法容纳下一条完整的消息，此处发生了“断包”
                    // 此时的buffer中，存放了一个消息体的开头的“部分数据”，我们需要将这“部分数据”缓存起来，如下
                    // buffer to cacheBuffer
                    cacheBuffer.put(buffer);
                    buffer.clear();
                    // bodyLength是一个消息体的总长度，既然上一行的代码已经读取了该消息体的部分数据，那么bodyLength也要同步地减少响应的字节数
                    bodyLength -= cacheBuffer.position();
                    // 此时的bodyLength不是-1，就会告诉下次的read操作，发生了断包！
                }
            } else {
                // 流程能走到这里，说明上一次的read发生了断包，那就接着上一次read，继续读取没有读取完的消息
                int n = channel.read(buffer);
                buffer.flip();
                if (bodyLength > n) {
                    // bodyLength > n，继续处于断包状态，继续把读取到的消息的部分数据缓存起来
                    cacheBuffer.put(buffer);
                    // 既然上一行代码读取完了buffer中的值，那么就执行buffer.clear()方法来“清空”buffer
                    // 这样才能让下次read可以继续向buffer写入数据。
                    buffer.clear();
                    bodyLength -= n;
                    // 经过计算，bodyLength仍然不会被重置为-1，旨在告诉下一次的read，本次read（仍然）发生了断包
                } else {
                    for (int i = 0; i < bodyLength; i++) {
                        cacheBuffer.put(buffer.get());
                    }
                    cacheBuffer.flip();
                    bodyBytes = new byte[bodyLength2];
                    cacheBuffer.get(bodyBytes);
                    String msg = new String(bodyBytes);
                    System.out.println(msg + " 断包处理");
                    cacheBuffer.clear();
                    buffer.compact();
                    bodyLength = -1;
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // ===============================read测试范围结束================================================


    private void accept(SelectionKey key) {
        try {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            SocketChannel channel = serverSocketChannel.accept();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
            System.out.println("Here comes a client");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        Server server = new Server(8888);
        server.start();
    }
}
